import React from 'react'

// 导航上下文
const NavigationContext = React.createContext()

// 路径上下文
const LocationContext = React.createContext()

// 路由信息上下文 存放当前路由信息 比如路径参数对象
const RouteContext = React.createContext()

/**
 * 最核心的路由容器组件
 * @param {*} children 子元素 
 * @param {*} location 浏览器地址的完整描述对象 
 * @param {*} navigator 历史对象 也称为导航对象
 */
export function Router({ children, location, navigator }) {
  // 在组件重新渲染时 只要 navigator 的值不发生改变 React.useMemo 不会重新执行回调函数
  // 导航上下文的初始值 { navigator: 导航对象 }
  const navigationValue = React.useMemo(() => ({ navigator }), [navigator])
  const LocationValue = React.useMemo(() => ({ location }), [location])

  return <NavigationContext.Provider value={navigationValue}>
    <LocationContext.Provider value={LocationValue}>
      {children}
    </LocationContext.Provider>
  </NavigationContext.Provider>
}


export function Routes({ children }) {
  // 把子元素转换成 routes 配置数组
  const routes = createRoutesFromChildren(children)
  // 使用 useRoutes 这个自定义 hooks 进行渲染
  return useRoutes(routes)
}

/**
 * 用于获取浏览器地址的完整描述对象
 * @returns 浏览器地址的完整描述对象
 */
export function useLocation() {
  // location = { pathname, state }
  return React.useContext(LocationContext).location
}

/**
 * 根据路由配置数组渲染对应的组件
 * @param {} routes routes 配置数组
 */
export function useRoutes(routes) {
  const location = useLocation() // 获取当前的路径对象
  const pathname = location.pathname // 获取浏览器地址栏中的路径名
  const matches = matchRoutes(routes, pathname) // 使用浏览器地地址栏中的路径匹配 routes
  console.log(matches, 'matches')
  // 渲染匹配的结果
  return _renderMatches(matches)
}

/**
 * 渲染匹配的结果
 * @param {*} matches [ { params: {}, route: {path: '/user', element: <User />} }, { params: { id: '1752997860889' }, route: { path: 'detail/:id' }, element: <UserDetail /> } ]
 */
function _renderMatches(matches) {
  if (!matches) return null
  return matches.reduceRight((outlet, match, index) => {
    // 外层包一个 <RouteContext.Provider> 组件用于拿到动态路径参数 + outlet 组件
    return <RouteContext.Provider value={{ outlet, matches: matches.slice(0, index + 1) }}>
      {match.route.element}
    </RouteContext.Provider>
  }, null)
}

/**
 * 使用浏览器地地址栏中的路径匹配 routes
 * @param {*} routes 配置数组
 * @param {*} pathname 浏览器地址栏中的路径
 */
function matchRoutes(routes, pathname) {
  const branches = flattenRoutes(routes) // 打平所有的路由分支
  console.log(branches, '打平的分支路径')
  rankRoutesBranches(branches) // 对打平的分支进行排序
  console.log(branches, '排序后的打平分支')
  let matches = null // 分支匹配的结果
  // 遍历每一个路由分支
  for (let i = 0; matches === null && i < branches.length; i++) {
    matches = matchRouteBranch(branches[i], pathname) // 匹配当前路径分支 与 浏览器地址栏 路径
  }
  return matches
}

/**
 * 对不同的分支按分数进行排序
 * @param {*} branches 路由分支组成的数组
 */
function rankRoutesBranches(branches) {
  // >0 则 a 在 b 后 比如 [b, a]
  // <0 则 a 在 b 前 比如 [a, b]
  // =0 则调用 compareIndexs 函数来决定排序顺序
  branches.sort((a, b) => {
    return a.score !== b.score ?
      // 如果两个分支分数不同 则比较分数 降序排列 ( 从大到小 )
      b.score - a.score :
      // 如果分数相同 由判断是不是兄弟路由
      // 如果是兄弟路由 索引小的优先级高
      // 如果不是兄弟路由 先书写的组件优先级高
      compareIndexs(
        a.routeMetas.map(meta => meta.childrenIndex), // 获取所有的路由元数据中的索引
        b.routeMetas.map(meta => meta.childrenIndex),
      )
  })
}

/**
 * 如果是兄弟路由 则索引越小级别越高\
 * 如果不是兄弟路由则认为两个路由权重分数相等 谁写在前面谁的路由权重就高
 * @param {*} a 路由分支中每个路由元数据中的 childrenIndex 属性的索引值组成的数组
 * @param {*} b 路由分支中每个路由元数据中的 childrenIndex 属性的索引值组成的数组
 * @returns 
 */
function compareIndexs(a, b) {
  let sibling = a.length === b.length &&
    a.
      slice(0, -1) // 截取路由分支中路由元数据中除最后一个路由元数据的 childrenIndex
      .every((n, index) => { // 如果是空数组 则返回 true
        return n === b[index] // 如果 a 与 b 的每个同级路由元数据 childrenIndex 相同则认为是兄弟路由
      })
  // 如果是兄弟路由 则索引越小级别越高
  // 如果不是兄弟路由则认为两个路由权重分数相等 谁写在前面谁的路由权重就高
  // >0 则 a 在 b 后 比如 [b, a]
  // <0 则 a 在 b 前 比如 [a, b]
  return sibling ? a[a.length - 1] - b[b.length - 1] : 0
}

/**
 * 匹配路由分支
 * @param {*} branch 路由分支
 * @param {*} pathname 浏览器地址栏路径 比如 "/user/add" 或 "/user/detail/1752624103192"
 */
function matchRouteBranch(branch, pathname) {
  const { routeMetas } = branch // 获取当前路由分支中的 路由元信息组成的数组
  let matchedPathname = '' // 已经匹配过的路径名
  const matches = [] // 匹配结果的数组 [{route: 路由对象, params: }]
  let matchedParams = {} // 路径动态参数 比如 detail/:id 中的 id 该变量会累加祖先的动态路径参数
  // 遍历 路由分支 里面的 路由元数据数组 获取每一个路由元数据
  for (let i = 0; i < routeMetas.length; i++) {
    const { route } = routeMetas[i] // 从路由元数据中 获取路由对象
    const end = i === routeMetas.length - 1 // 判断此 meta 是不是路由元数据数组中最后一个路由元数据
    // 用完整路径截掉已经匹配的路径 得到剩下的 remainingPathname 路径
    const remainingPathname = pathname.slice(matchedPathname.length)
    // 把路由对象的路径和剩下等待匹配的路径进行匹配 得到匹配的结果
    let match = matchPath({ path: route.path, end }, remainingPathname)
    // 如果 match 为空 直接返回 null 代表此路径不通
    if (!match) return null
    // 合并路径参数对象 {} {id: 100} {id: 100, age: 66}
    matchedParams = Object.assign({}, matchedParams, match.params)
    matches.push({ route, params: matchedParams }) // 路由对象 + 路径参数对象保存到 matches 中
    // 用上一段匹配的路径加上此route匹配的路径 会成为下一次已经匹配的路径
    matchedPathname = joinPaths([matchedPathname, match.matchedPathname])
  }
  return matches
}

/**
 * 如果 s 是 '*' 则返回 true
 * @param {*} s 路径
 * @returns boolean
 */
function isSplat(s) {
  return s === '*'
}
/**
 * 通配符对应的惩罚分数
 */
const splatPenalty = -2

/**
 * 索引路径的值
 */
const indexRouteValue = 2

/**
 * 匹配路径参数的正则表达式\
 * \w 代表 大小写字母 数字 _
 */
const paramRegexp = /^:\w+$/

/**
 * 动态路径的分数值
 */
const dynamicSegmentValue = 3

/**
 * 路径参数是空串分数值
 */
const emptySegmentValue = 1

/**
 * 静态路径的分数值
 */
const staticsSegmentValue = 10

/**
 * 计算路由(分支)路径权重 分数
 * @param {*} path 当前路由被访问的完整路径
 * @param {*} index 当前路由在兄弟路由中的索引位置
 */
function computeScore(path, index) {
  // 把路由访问的完整路径用 / 分割
  // '/user/detail/:id' 转成为 ['', 'user', 'detail', ':id'] 里面每个元素的值都是路径分片
  const segments = path.split('/')
  let initialScore = segments.length // 分片的长度是初始化分类 4
  // 判断路径分片的值是不是 '*'
  if (segments.some(isSplat)) {
    initialScore += splatPenalty // 路径分片的值是 '*' 则减去 2
  }
  if (index) { // 兄弟路由中 除了第一个路径以外 其他的路由都添加 2
    initialScore += indexRouteValue
  }
  let scope = segments
    .filter(s => !isSplat(s)) // 过滤掉路径分片中值为 '*' 的分片
    .reduce((score, segment) => { // 调用 reduce 的数组如果是一个空数组则返回 initialScore 值
      return score + (
        // 动态路径 + 3
        // 空路径 + 1
        // 静态路径 + 10
        paramRegexp.test(segment) ? dynamicSegmentValue :
          segment === '' ? emptySegmentValue : staticsSegmentValue
      )
    }, initialScore)
  return scope
}

/**
 * 打平所有的路径分支并返回 branches
 * @param {*} routes 配置对象 [{path, element, children?}]
 * @param {*} branches 路由分支数组
 * @param {*} parentMetas 包含祖先的路由元数据
 * @param {*} parentPath 祖先路由的访问路径
 */
function flattenRoutes(routes, branches = [], parentMetas = [], parentPath = '') {
  // 遍历配置对象 获取每个路由
  routes.forEach((route, index) => {
    let routeMeta = { // 路由对象元数据 里面包含了 route 路由对象
      route, // 当前路由对象
      childrenIndex: index, // 当前路由在兄弟路由中的索引位置
    }
    const routePath = joinPaths([parentPath, route.path]) // 当前路由被访问的完整路径
    const routeMetas = [...parentMetas, routeMeta] // 当前的路由对象元数据包含了祖先路由对象元数据
    if (route.children) { // 处理子路由情况 
      // 将 routeMetas 父路由对象元数据 和 父路由被访问的 routePath 路径 作为参数传递
      flattenRoutes(route.children, branches, routeMetas, routePath)
    }
    branches.push({
      routePath, // 当前路由被访问的完整路径
      routeMetas, // 路由元信息组成的数组
      score: computeScore(routePath, index) // 计算路由分支权重分数
    })
  })
  return branches
}

/**
 * 将数组中的路径合并成一个路径
 * @param {*} paths 多个路径组成的数组
 */
function joinPaths(paths) {
  // 将多个 / 变成一个 /
  return paths.join('/').replace(/\/\/+/g, '/') // 并返回最终的路径
}

/**
 * 匹配路径
 * @param {*} path 路由自身路径
 * @param {*} end 是不是路由元数据数组中最后一个路由元数据
 * @param {*} pathname 需要匹配的路径
 */
function matchPath({ path, end }, pathname) {
  const [matcher, paramNames] = compilePath(path, end) // 基于路由自身路径编译出一个正则 
  const match = pathname.match(matcher) // 调用字符串的 match 方法
  if (!match) return null // 没有匹配到返回 null
  const [matchedPathname, ...values] = match
  const params = paramNames.reduce((memo, paramName, index) => {
    memo[paramName] = values[index]
    return memo
  }, {})
  return {
    params, // 动态路径参数 + 动态路径参数的值 组成的参数对象
    matchedPathname, // 匹配到的路径
  }
}

/**
 * 将 route 中的 path 属性编译成正则表达式
 * @param {*} path 匹配的路径
 */
function compilePath(path, end) { // path 需要支持路径参数
  let paramNames = [] // 保存路径参数名的数组
  let regexpSource = `^` + path
    // 将path匹配路径的 ":id" 动态路径参数部分替换为 "([^\/#\?]+)"
    .replace(/:(\w+)/g, (_, group1) => {
      // \w  大小写字母和数字和_
      // console.log(_, group1) // :id id
      paramNames.push(group1) // 把路径参数 id 保存到数组里面
      // 返回值会替换匹配到的字符串
      // () 代表一个捕获组
      // [^\/#\?] 代表匹配任何不是 / 或 # 或 ? 的字符
      // + 一个或多个 贪婪匹配
      return "([^\/#\?]+)"
    })
    // 处理路径头部有1个或多个或没有 / 的情况 将路径头部统一成一个 /
    // 比如 "/add" 路径变成 "/add" 比如 "list" 路径变成 "/list" 
    .replace(/^\/*/, '/') // 把开头的 0个 / 或 1个 / 或 多个 / 全部转换为1个 /
  if (end) { // 处理子路由的最后一个路由情况
    // 字符串中 两个 \ 其中第一个 \ 代表转义字符 用于转义第二个 \ 
    regexpSource += '\\/*$' // 匹配结尾 0个 / 或 1个 / 或 多个 / 的路径
  }
  if (path === '*') { // 处理 404 组件情况
    // . 代表任意字符
    // * 前面一个元素出现 0次 或 多次
    regexpSource = '.*'
  }
  let matcher = new RegExp(regexpSource)
  return [matcher, paramNames]
}

/**
 * 是一个 react 函数式组件空实现 里面什么都没有
 */
export function Route() {
  return null
}

/**
 * 把子元素转换成 routes 配置数组
 */
function createRoutesFromChildren(children) {
  console.log('children ', children)
  let routes = [] // 用于保存配置数组
  // 使用 React.Children.forEach 内置方法来遍历每个子元素
  React.Children.forEach(children, ({ props: { path, element, children } }) => {
    let route = {
      path: path, // 获取路径
      element, // 获取虚拟DOM
    }
    // 是否存在子路由
    if (children) { // 存在子路由 扩展一个 children 属性
      route.children = createRoutesFromChildren(children)
    }
    routes.push(route)
  })
  return routes
}

export function useNavigate() {
  const { navigator } = React.useContext(NavigationContext)
  // 缓存函数 只有当依赖发生变化时才会重新生成
  let navigate = React.useCallback((to) => {
    navigator.push(to)
  }, [navigator])
  return navigate
}

/**
 * 把子路由对应的组件渲染出来 todo
 */
export function Outlet() {
  return useOutlet()
}

/**
 * 获取并返回 RouteContext 上下文中值中的 outlet 属性的值
 */
function useOutlet() {
  const { outlet } = React.useContext(RouteContext)
  return outlet
}

/**
 * 从上下文中获取路径参数
 */
export function useParams() {
  const { matches } = React.useContext(RouteContext)
  return matches[matches.length - 1].params
}
